package top.gytf.unifiedcall.jsr269.exception;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.squareup.javapoet.*;
import top.gytf.unifiedcall.jsr269.AbstractUnifiedCallProcessor;
import top.gytf.unifiedcall.jsr269.Utils;

import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

import static java.lang.String.format;

/**
 * 用于异常生成的注解处理器
 */
public class ExceptionGenerateProcessor extends AbstractUnifiedCallProcessor {
    private static final ClassName BASE_EXCEPTION = ClassName.bestGuess("top.gytf.unifiedcall.model.exception.BaseException");

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return CollUtil.newHashSet(GenException.class.getName());
    }

    @Override
    public int order() {
        return 0;
    }

    @Override
    public void process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(GenException.class);

        for (Element element : elements) {
            List<VariableElement> candidates = findCandidates(element);
            if (CollUtil.isEmpty(candidates)) {
                continue;
            }
            GenException annotation = element.getAnnotation(GenException.class);
            candidates.forEach(candidate -> generateException(candidate, annotation));
        }
    }

    /**
     * 检查给定Element是否需要被处理
     *
     * @return 符合要求的 StatusCode 枚举值 Element列表
     */
    private List<VariableElement> findCandidates(Element enumElement) {
        return Utils.scanStatusCodes(enumElement, messager,
                // 标注这个注解的不需要生成异常
                e -> e.getAnnotation(GenException.Ignore.class) == null);
    }

    /**
     * 生成对应的异常
     *
     * @param enumConstElement 实现StatusCode的枚举常量
     * @param anno             注解在枚举类上的标记配置注解
     */
    private void generateException(VariableElement enumConstElement, GenException anno) {
        // 生成名称
        String simpleClassName = generateClassName(enumConstElement);
        String packageName = generatePackageName(enumConstElement, anno);
        String className = CharSequenceUtil.isEmpty(packageName) ? simpleClassName : packageName + "." + simpleClassName;

        // 检查是否已生成
        TypeElement existed = elementUtils.getTypeElement(className);
        if (existed != null) {
            GenException.RepeatPolicy repeatPolicy = anno.repeatPolicy();
            if (GenException.RepeatPolicy.ERROR.equals(repeatPolicy)) {
                messager.printMessage(Diagnostic.Kind.ERROR, format("生成[%s]失败：名称已被占用", className));
            } else if (GenException.RepeatPolicy.SKIP.equals(repeatPolicy)) {
                messager.printMessage(Diagnostic.Kind.WARNING, format("跳过生成[%s]：名称已被占用", className));
            }
            return;
        }

        // 生成类
        TypeSpec typeSpec = TypeSpec.classBuilder(simpleClassName)
                .addModifiers(Modifier.PUBLIC)
                .superclass(BASE_EXCEPTION)
                .addMethods(generateConstructors(enumConstElement))
                .addJavadoc("Auto Generate by UnifiedCall.")
                .build();

        JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
                .build();
        try {
            // 保存
            javaFile.writeTo(filer);
        } catch (IOException e) {
            messager.printMessage(Diagnostic.Kind.ERROR, e.getLocalizedMessage());
        }
    }

    /**
     * 从EnumConstant生成对应的Exception类名(转大驼峰后拼接Exception后缀)
     */
    private String generateClassName(VariableElement element) {
        String constName = element.getSimpleName().toString();
        return Arrays.stream(constName.split("_"))
                .map(str -> str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase())
                .collect(Collectors.joining()) + "Exception";
    }

    /**
     * 根据指定的包名生成异常类所在软件包
     */
    private String generatePackageName(VariableElement enumConstElement, GenException anno) {
        if (GenException.UNSPECIFIED_PACKAGE.equals(anno.basePackage())) {
            Element enclosing = enumConstElement.getEnclosingElement();
            while (!ElementKind.PACKAGE.equals(enclosing.getKind())) {
                enclosing = enclosing.getEnclosingElement();
            }
            return Objects.toString(enclosing);
        }
        return anno.basePackage();
    }

    /**
     * 生成BaseException的各个构造器
     */
    private Collection<MethodSpec> generateConstructors(VariableElement enumConstElement) {
        Element enclosingElement = enumConstElement.getEnclosingElement();

        ArrayList<MethodSpec> constructors = new ArrayList<>(5);

        ParameterSpec message = ParameterSpec.builder(String.class, "message").build();
        ParameterSpec data = ParameterSpec.builder(Object.class, "data").build();
        ParameterSpec cause = ParameterSpec.builder(Throwable.class, "cause").build();
        ParameterSpec enableSuppression = ParameterSpec.builder(boolean.class, "enableSuppression").build();
        ParameterSpec writableStackTrace = ParameterSpec.builder(boolean.class, "writableStackTrace").build();

        // 无参
        MethodSpec noArgsConstructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addCode(CodeBlock.builder()
                        .addStatement("super($T.$L)", enclosingElement, enumConstElement)
                        .build())
                .build();
        constructors.add(noArgsConstructor);

        // message构造
        MethodSpec messageConstructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(message)
                .addStatement("super($T.$L, $N)", enclosingElement, enumConstElement, message)
                .build();
        constructors.add(messageConstructor);

        // data构造
        MethodSpec dataConstructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(data)
                .addStatement("super($T.$L, $N)", enclosingElement, enumConstElement, data)
                .build();
        constructors.add(dataConstructor);

        // cause构造
        MethodSpec causeConstructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(cause)
                .addStatement("super($T.$L, $N)", enclosingElement, enumConstElement, cause)
                .build();
        constructors.add(causeConstructor);

        // message和cause构造
        MethodSpec messageAndCauseConstructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(message)
                .addParameter(cause)
                .addStatement("super($T.$L, $N, $N)", enclosingElement, enumConstElement, message, cause)
                .build();
        constructors.add(messageAndCauseConstructor);

        // message和data构造
        MethodSpec messageAndDataConstructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(message)
                .addParameter(data)
                .addStatement("super($T.$L, $N, $N)", enclosingElement, enumConstElement, message, data)
                .build();
        constructors.add(messageAndDataConstructor);

        // message、data和cause构造
        MethodSpec messageAndDataAndCauseConstructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(message)
                .addParameter(data)
                .addParameter(cause)
                .addStatement("super($T.$L, $N, $N, $N)", enclosingElement, enumConstElement, message, data, cause)
                .build();
        constructors.add(messageAndDataAndCauseConstructor);

        // 全参
        MethodSpec fullArgsConstructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(message)
                .addParameter(data)
                .addParameter(cause)
                .addParameter(enableSuppression)
                .addParameter(writableStackTrace)
                .addStatement("super($T.$L, $N, $N, $N, $N, $N)", enclosingElement, enumConstElement,
                        message, data, cause, enableSuppression, writableStackTrace)
                .build();
        constructors.add(fullArgsConstructor);

        return constructors;
    }
}
